// This file is part of LeanSDR Copyright (C) 2016-2018 <pabr@pabr.org>.
// See the toplevel README for more information.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.


#ifndef LEANSDR_GUI_H
#define LEANSDR_GUI_H

#include <sys/time.h>

#include "framework.h"

namespace leansdr {

  //////////////////////////////////////////////////////////////////////
  // GUI blocks
  //////////////////////////////////////////////////////////////////////
  
#ifdef GUI
  
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
  
  static const int DEFAULT_GUI_DECIMATION = 64;
  
  struct gfx {
    Display *display;
    int screen;
    int w, h;
    Window window;
    GC gc;
    Pixmap dbuf;
    gfx(scheduler *sch, const char *name) {
      window_placement *wp;
      for ( wp=sch->windows; wp && wp->name; ++wp )
	if ( ! strcmp(wp->name, name) ) break;
      if ( wp && wp->name )
	init(wp->name, wp->x, wp->y, wp->w, wp->h);
      else {
	fprintf(stderr, "No placement hints for window '%s'\n", name);
	init(name, -1, -1, 320, 240);
      }
    }
    gfx(const char *name, int _x, int _y, int _w, int _h) {
      init(name, _x, _y, _w, _h);
    }
    void init(const char *name, int _x, int _y, int _w, int _h) {
      buttons = 0;
      clicks = 0;
      mmoved = false;
      w = _w;
      h = _h;
      display = XOpenDisplay(getenv("DISPLAY"));
      if ( ! display ) fatal("display");
      screen = DefaultScreen(display);
      XSetWindowAttributes xswa;
      xswa.event_mask = (ExposureMask|
			 StructureNotifyMask|
			 ButtonPressMask|
			 ButtonReleaseMask|
			 KeyPressMask|
			 KeyReleaseMask|
			 PointerMotionMask);
      xswa.background_pixel = BlackPixel(display, screen);
      window = XCreateWindow(display, DefaultRootWindow(display), 
			     100,100, w,h, 10, CopyFromParent,InputOutput,
			     CopyFromParent, CWEventMask|CWBackPixel,
			     &xswa);
      if ( !window ) fatal("window");
      XStoreName(display, window, name);
      XMapWindow(display, window);
      if ( _x>=0 && _y>=0 )
	XMoveWindow(display, window, _x, _y);
      dbuf = XCreatePixmap(display, window, w, h, DefaultDepth(display,screen));
      gc = XCreateGC(display, dbuf, 0, NULL);
      if ( ! gc ) fatal("gc");
    }
    void clear() {
      setfg(0, 0, 0);
      XFillRectangle(display, dbuf, gc, 0, 0, w, h);
    }
    void show() {
      XCopyArea(display, dbuf, window, gc, 0, 0, w, h, 0, 0);
    }
    void sync() {
      XSync(display, False);
    }
    void events() {
      XEvent ev;
      while ( XCheckWindowEvent(display, window, -1, &ev) ) {
	switch ( ev.type ) {
	case ButtonPress: {
	  int b = ev.xbutton.button;
	  buttons |= 1<<b;
	  clicks |= 1<<b;
	  mx = ev.xbutton.x;
	  my = ev.xbutton.y;
	  break;
	}
	case ButtonRelease: {
	  int b = ev.xbutton.button;
	  buttons &= ~(1<<b);
	  mx = ev.xbutton.x;
	  my = ev.xbutton.y;
	  break;
	}
	case MotionNotify:
	  mx = ev.xbutton.x;
	  my = ev.xbutton.y;
	  mmoved = true;
	  break;
	}
      }
    }
    void setfg(unsigned char r, unsigned char g, unsigned char b) {
      XColor c;
      c.red = r<<8; c.green = g<<8; c.blue = b<<8;
      c.flags = DoRed | DoGreen | DoBlue;
      if ( ! XAllocColor(display, DefaultColormap(display,screen), &c) )
	fatal("color");
      XSetForeground(display, gc, c.pixel);
    }
    void point(int x, int y) {
      XDrawPoint(display, dbuf, gc, x, y);
    }
    void line(int x0, int y0, int x1, int y1) {
      XDrawLine(display, dbuf, gc, x0,y0, x1,y1);
    }
    void text(int x, int y, const char *s) {
      XDrawString(display, dbuf, gc, x,y, s, strlen(s));
    }
    void transient_text(int x, int y, const char *s) {
      XDrawString(display, window, gc, x,y, s, strlen(s));
    }
    int buttons;  // Mask of button states (2|4|8)
    int clicks;   // Same, accumulated (must be cleared by owner)
    int mx, my;   // Cursor position
    bool mmoved;  // Pointer moved (must be cleared by owner)
  };
  
  template<typename T>
  struct cscope : runnable {
    T xymin, xymax;
    unsigned long decimation;
    unsigned long pixels_per_frame;
    cstln_lut<256> **cstln;  // Optional ptr to optional constellation
    cscope(scheduler *sch, pipebuf< complex<T> > &_in, T _xymin, T _xymax,
	   const char *_name=NULL)
      : runnable(sch, _name?_name:_in.name),
	xymin(_xymin), xymax(_xymax),
	decimation(DEFAULT_GUI_DECIMATION), pixels_per_frame(1024),
	cstln(NULL),
	in(_in), phase(0), g(sch, name) {
    }
    void run() {
      while ( in.readable() >= pixels_per_frame ) {
	if ( ! phase ) {
	  draw_begin();
	  g.setfg(0, 255, 0);
	  complex<T> *p = in.rd(), *pend = p+pixels_per_frame;
	  for ( ; p<pend; ++p )
	    g.point(g.w*(p->re-xymin)/(xymax-xymin),
		    g.h - g.h*(p->im-xymin)/(xymax-xymin));
	  if ( cstln && (*cstln) ) {
	    // Plot constellation points
	    g.setfg(255, 255, 255);
	    for ( int i=0; i<(*cstln)->nsymbols; ++i ) {
	      complex<signed char> *p = &(*cstln)->symbols[i];
	      int x = g.w*(p->re-xymin)/(xymax-xymin);
	      int y = g.h - g.h*(p->im-xymin)/(xymax-xymin);
	      for ( int d=-2; d<=2; ++d ) {
		g.point(x+d, y);
		g.point(x, y+d);
	      }
	    }
	  }
	  g.show();
	  g.sync();
	}
	in.read(pixels_per_frame);
	if ( ++phase >= decimation ) phase = 0;
      }
    }
    //private:
    pipereader< complex<T> > in;
    unsigned long phase;
    gfx g;
    void draw_begin() {
      g.clear();
      g.setfg(0, 255, 0);
      g.line(g.w/2,0, g.w/2, g.h);
      g.line(0,g.h/2, g.w,g.h/2);
    }	
  };
  
  template<typename T>
  struct wavescope : runnable {
    T ymin, ymax;
    unsigned long decimation;
    wavescope(scheduler *sch, pipebuf<T> &_in,
	      T _ymin, T _ymax, const char *_name=NULL)
      : runnable(sch, _name?_name:_in.name),
	in(_in), ymin(_ymin), ymax(_ymax),
	decimation(DEFAULT_GUI_DECIMATION),
	g(sch, name), phase(0),
	x(0) {
      g.clear();
    }	
    void run() {
      while ( in.readable() >= g.w ) {
	if ( ! phase ) plot(in.rd(), g.w);
	in.read(g.w);
	if ( ++phase >= decimation ) phase = 0;
      }
    }
    void plot(T *p, int count) {
      T *pend = p + count;
      g.clear();
      g.setfg(0, 255, 0);
      for ( int x=0; p<pend; ++x,++p ) {
	T v = *p;
	g.point(x, g.h-1 - (g.h-1)*(v-ymin)/(ymax-ymin));
      }
      g.show();
      g.sync();
    }

  private:
    pipereader<T> in;
    int phase;
    gfx g;
    int x;
  };

  template<typename T>
  struct slowmultiscope : runnable {
    struct chanspec {
      pipebuf<T> *in;
      const char *name, *format;
      unsigned char rgb[3];
      float scale;
      float ymin, ymax;
      enum flag {
	DEFAULT  = 0,
	ASYNC    = 1,  // Read whatever is available
	COUNT    = 2,  // Display number of items read instead of value
	SUM      = 4,  // Display sum of values
	LINE     = 8,  // Connect points
	WRAP     = 16, // Modulo min..max
	DISABLED = 32, // Ignore channel
      } flags;
    };
    unsigned long samples_per_pixel;
    float sample_freq;  // Sample rate in Hz (used for cursor operations)
    slowmultiscope(scheduler *sch, const chanspec *specs, int nspecs,
		   const char *_name)
      : runnable(sch, _name?_name:"slowmultiscope"),
	samples_per_pixel(1), sample_freq(1),
	g(sch, name), t(0), x(0), total_samples(0) {
      chans = new channel[nspecs];
      nchans = 0;
      for ( int i=0; i<nspecs; ++i ) {
	if ( specs[i].flags & chanspec::DISABLED ) continue;
	chans[nchans].spec = specs[i];
	chans[nchans].in = new pipereader<T>(*specs[i].in);
	chans[nchans].accum = 0;
	++nchans;
      }
      g.clear();
    }
    void run() {
      // Read up to one pixel worth of data
      unsigned long count = samples_per_pixel;
      for ( channel *c=chans; c<chans+nchans; ++c )
	if ( ! (c->spec.flags&chanspec::ASYNC) )
	  count = min(count, c->in->readable());
      for ( int n=count; n--; ) {
	for ( channel *c=chans; c<chans+nchans; ++c ) {
	  int nr;
	  if ( c->spec.flags & chanspec::ASYNC )
	    // For async channels, read any and all available data.
	    nr = c->in->readable();
	  else
	    nr = 1;
	  g.setfg(c->spec.rgb[0], c->spec.rgb[1], c->spec.rgb[2]);
	  int y = -1;
	  while ( nr-- ) {
	    float v = *c->in->rd() * c->spec.scale;
	    if ( c->spec.flags & chanspec::COUNT )
	      ++c->accum;
	    else if ( c->spec.flags & chanspec::SUM )
	      c->accum += v;
	    else {
	      c->print_val = v;
	      float nv = (v-c->spec.ymin) / (c->spec.ymax-c->spec.ymin);
	      if ( c->spec.flags & chanspec::WRAP )
		nv = nv - floor(nv);
	      y = g.h - g.h*nv;
	    }
	    c->in->read(1);
	  }
	  // Display count/sum channels only when the cursor is about to move.
	  if ( (c->spec.flags&(chanspec::COUNT|chanspec::SUM)) &&
	       t+1 >= samples_per_pixel ) {
	    T v = c->accum;
	    y = g.h-1 - g.h*(v-c->spec.ymin)/(c->spec.ymax-c->spec.ymin);
	    c->accum = 0;
	    c->print_val = v;
	  } 
	  if ( y >= 0 ) {
	    if ( c->spec.flags & chanspec::LINE ) {
	      if ( x ) g.line(x-1, c->prev_y, x, y);
	      c->prev_y = y;
	    } else
	      g.point(x, y);
	  }
	}
	g.show();
	// Print instantatenous values as text
	for ( int i=0; i<nchans; ++i ) {
	  channel *c = &chans[i];
	  g.setfg(c->spec.rgb[0], c->spec.rgb[1], c->spec.rgb[2]);
	  char text[256];
	  sprintf(text, c->spec.format, c->print_val);
	  g.transient_text(5, 20+16*i, text);
	}
	run_gui();
	if ( ++t >= samples_per_pixel ) {
	  t = 0;
	  ++x;
	  if ( x >= g.w ) x = 0;
	  g.setfg(0, 0, 0);
	  g.line(x, 0, x, g.h-1);
	}
	run_gui();
	g.sync();
      }
      total_samples += count;
    }
    void run_gui() {
      g.events();
      // Print cursor time
      float ct = g.mx * samples_per_pixel / sample_freq;
      float tt = total_samples / sample_freq;
      char text[256];
      sprintf(text, "%.3f / %.3f s", ct, tt);
      g.setfg(255, 255, 255);
      g.transient_text(g.w*3/4, 20, text);
    }
  private:
    int nchans;
    struct channel {
      chanspec spec;
      pipereader<T> *in;
      float accum;
      int prev_y;
      float print_val;
    } *chans;
    gfx g;
    unsigned long t;
    int x;
    int total_samples;
  };
  
  template<typename T>
  struct spectrumscope : runnable {
    T ymax;
    float amax;
    unsigned long size;
    unsigned long decimation;
    spectrumscope(scheduler *sch, pipebuf< complex<T> > & _in,
		  T _max, const char *_name=NULL)
      : runnable(sch, _name?_name:_in.name),
	ymax(_max), amax(_max),
	size(4096), decimation(DEFAULT_GUI_DECIMATION),
	in(_in), phase(0), g(sch, name), fft(NULL) {
    }
    void run() {
      while ( in.readable() >= size ) {
	if ( ! phase ) do_fft(in.rd());
	in.read(size);
	if ( ++phase >= decimation ) phase = 0;
      }
    }
  private:
    pipereader< complex<T> > in;
    int phase;
    gfx g;
    cfft_engine<float> *fft;
    void do_fft(complex<T> *input) {
      draw_begin();
      if ( !fft || fft->n!=size ) {
	if ( fft ) delete fft;
	fft = new cfft_engine<float>(size);
      }
      complex<T> *pin=input, *pend=pin+size;
      complex<float> data[size], *pout=data;
      g.setfg(255, 0, 0);
      for ( int x=0; pin<pend; ++pin,++pout,++x ) {
	pout->re = (float)pin->re;
	pout->im = (float)pin->im;
	// g.point(x, g.h/2-pout->re*g.h/2/ymax);
      }
      fft->inplace(data, true);
      g.setfg(0, 255, 0);
      for ( int i=0; i<size; ++i ) {
	int x = ((i<size/2)?i+size/2:i-size/2) * g.w / size;
	complex<float> v = data[i];;
	float y = hypot(v.re, v.im);
	g.line(x, g.h-1, x, g.h-1-y*g.h/amax);
      }
      if ( g.buttons ) {
	char s[256];
	float f = 2.4e6 * (g.mx-g.w/2) / g.w;
	sprintf(s, "%f", f);
	g.text(16, 16, s);
      }
      g.show();
      g.sync();
    }
    void draw_begin() {
      g.clear();
      g.setfg(255, 255, 255);
      g.line(g.w/2,0, g.w/2,g.h);
    }
  };
  
  template<typename T>
  struct rfscope : runnable {
    unsigned long size;
    unsigned long decimation;
    float Fs;  // Sampling freq for display (Hz)
    float Fc;  // Center freq for display (Hz)
    int ncursors;
    float *cursors;     // Cursor frequencies
    float hzoom;        // Horizontal zoom factor
    float db0, dbrange; // Vertical range db0 .. db0+dbrange
    float bw;           // Smoothing bandwidth
    rfscope(scheduler *sch, pipebuf< complex<T> > & _in,
	    const char *_name=NULL)
      : runnable(sch, _name?_name:_in.name),
	size(4096), decimation(DEFAULT_GUI_DECIMATION),
	Fs(1), Fc(0), ncursors(0), hzoom(1),
	db0(-25), dbrange(50), bw(0.05),
	in(_in), phase(0), g(sch, name), fft(NULL), filtered(NULL) {
    }
    void run() {
      while ( in.readable() >= size ) {
	if ( ! phase ) do_fft(in.rd());
	in.read(size);
	if ( ++phase >= decimation ) phase = 0;
      }
    }
  private:
    pipereader< complex<T> > in;
    int phase;
    gfx g;
    cfft_engine<float> *fft;
    float *filtered;

    void do_fft(complex<T> *input) {
      g.events();
      draw_begin();
      if ( !fft || fft->n!=size ) {
	if ( fft ) delete fft;
	fft = new cfft_engine<float>(size);
      }
      // Convert to complex<float> and transform
      complex<T> *pin=input, *pend=pin+size;
      complex<float> data[size], *pout=data;
      for ( int x=0; pin<pend; ++pin,++pout,++x ) {
	pout->re = (float)pin->re;
	pout->im = (float)pin->im;
      }
      fft->inplace(data, true);
      float amp2[size];
      for ( int i=0; i<size; ++i ) {
	complex<float> &v = data[i];;
	amp2[i] = (v.re*v.re + v.im*v.im)*size;
      }
      if ( ! filtered ) {
	filtered = new float[size];
	for ( int i=0; i<size; ++i ) filtered[i] = amp2[i];
      }
      float bwcomp = 1 - bw;
      g.setfg(0, 255, 0);
      for ( int i=0; i<size; ++i ) {
	filtered[i] = amp2[i]*bw + filtered[i]*bwcomp;
	float db = filtered[i] ? 10 * logf(filtered[i])/logf(10) : db0;
	int is = (i<size/2) ? i : i-size;
	int x = g.w/2 + is*hzoom*g.w/size;
	int y = g.h-1 - (db-db0)*g.h/dbrange;
	g.line(x, g.h-1, x, y);
      }
      if ( g.buttons ) {
	char s[256];
	float freq = Fc + Fs*(g.mx-g.w/2)/g.w/hzoom;
	float val = db0 + (float)((g.h-1)-g.my)*dbrange/g.h;
	sprintf(s, "%f.3 Hz   %f.2 dB", freq, val);
	g.setfg(255, 255, 255);
	g.text(16, 16, s);
      }
      // Draw cursors
      g.setfg(255, 255, 0);
      for ( int i=0; i<ncursors; ++i ) {
	int x = g.w/2 + (cursors[i]-Fc)*hzoom*g.w/Fs;
	g.line(x,0, x,g.h-1);
      }
      g.show();
      g.sync();
    }
    void draw_begin() {
      g.clear();
      // dB scale
      g.setfg(64, 64, 64);
      for ( float db=floorf(db0); db<db0+dbrange; ++db ) {
	int y = g.h-1 - (db-db0)*g.h/dbrange;
	g.line(0,y, g.w-1,y);
      }
      // DC line
      g.setfg(255, 255, 255);
      g.line(g.w/2,0, g.w/2,g.h);
    }
  };

  template<typename T>
  struct genscope : runnable {
    struct render {
      int x, y;
      char dir;  // 'h'orizontal or 'v'ertical
    };
    struct chanspec {
      pipebuf<T> *in;  // NULL if disabled
      render r;
    };
    genscope(scheduler *sch, chanspec *specs, int _nchans,
	     const char *_name=NULL)
      : runnable(sch, _name?_name:"genscope"),
	nchans(_nchans),
	g(sch, name) {
      chans = new channel[nchans];
      for ( int i=0; i<nchans; ++i ) {
	if ( ! specs[i].in ) {
	  chans[i].in = NULL;
	} else {
	  chans[i].spec = specs[i];
	  chans[i].in = new pipereader<T>(*specs[i].in);
	}
      }
      g.clear();
      gettimeofday(&tv, NULL);
    }
    struct channel {
      chanspec spec;
      pipereader<T> *in;
    } *chans;
    int nchans;
    struct timeval tv;
    void run() {
      g.setfg(0, 255, 0);
      for ( channel *pc=chans; pc<chans+nchans; ++pc ) {
	if ( ! pc->in ) continue;
	int n = pc->in->readable();
	T last = pc->in->rd()[n-1];
	pc->in->read(n);
	int dx = pc->spec.r.dir=='h' ? last : 0;
	int dy = pc->spec.r.dir=='v' ? last : 0;
	dx /= 3;
	dy /= 3;
	g.line(pc->spec.r.x-dx, pc->spec.r.y-dy,
	       pc->spec.r.x+dx, pc->spec.r.y+dy);
	char txt[16];
	sprintf(txt, "%d", (int)last);
	g.text(pc->spec.r.x+5, pc->spec.r.y-2, txt);
      }
      struct timeval newtv;
      gettimeofday(&newtv, NULL);
      int dt = (newtv.tv_sec-tv.tv_sec)*1000 + (newtv.tv_usec-tv.tv_usec)/1000;
      if ( dt > 100 ) {
	fprintf(stderr, "#");
	g.show();
	g.sync();
	g.clear();
	tv = newtv;
      }
    }
  private:
    gfx g;
  };

#endif  // GUI

}  // namespace

#endif  // LEANSDR_GUI_H
